package rest

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"path"
	"reflect"
	"strings"
	"time"

	"gitee.com/youkelike/ziam/zsdk2/third_party/forked/gorequest"
	"gitee.com/youkelike/ziam/zutils/auth"
	"gitee.com/youkelike/ziam/zutils/runtime"
	"gitee.com/youkelike/zlog"
)

type Request struct {
	c *RESTClient

	timeout time.Duration

	// generic components accessible via method setters
	verb       string
	pathPrefix string

	// query 参数
	params  url.Values
	headers http.Header

	// 资源
	resource string
	// 资源名
	resourceName string
	// 子资源
	subresource string
	// 子资源名
	subpath string

	// output
	err  error
	body interface{}
}

func NewRequest(c *RESTClient) *Request {
	// 拼成类似 /v1 这样的路径
	var pathPrefix string
	if c.base != nil {
		pathPrefix = path.Join("/", c.base.Path, c.versionedAPIPath)
	} else {
		pathPrefix = path.Join("/", c.versionedAPIPath)
	}

	r := &Request{
		c:          c,
		pathPrefix: pathPrefix,
	}

	switch {
	case c.content.HasKeyAuth():
		// authz
		tokenString := auth.Sign(c.content.SecretID, c.content.SecretKey, "zsdk2", c.group+".ziam.com")
		r.SetHeader("Authorization", fmt.Sprintf("Bearer %s", tokenString))
	case c.content.HasTokenAuth():
		// apiserver
		r.SetHeader("Authorization", fmt.Sprintf("Bearer %s", c.content.BearerToken))
	}

	// switch c.group {
	// case "apiserver":
	// 	r.SetHeader("Authorization", fmt.Sprintf("Bearer %s", c.content.BearerToken))
	// case "authz":
	// 	tokenString := auth.Sign(c.content.SecretID, c.content.SecretKey, "zsdk2", c.group+".ziam.com")
	// 	r.SetHeader("Authorization", fmt.Sprintf("Bearer %s", tokenString))
	// 	// case c.content.HasBasicAuth():
	// 	// 	// TODO: get token and set header
	// 	// 	r.SetHeader("Authorization", "Basic "+basicAuth(c.content.Username, c.content.Password))
	// }

	// set accept content
	switch {
	case len(c.content.AcceptContentTypes) > 0:
		r.SetHeader("Accept", c.content.AcceptContentTypes)
	case len(c.content.ContentType) > 0:
		r.SetHeader("Accept", c.content.ContentType+", */*")
	}

	return r
}

// func basicAuth(username, password string) string {
// 	auth := username + ":" + password
// 	return base64.StdEncoding.EncodeToString([]byte(auth))
// }

// 不对外提供这个方法，这个方法参数也太多了，没有校验容易出错
// func NewRequestWithClient(base *url.URL, versionedAPIPath string,
// 	content ClientContentConfig, client *gorequest.SuperAgent) *Request {
// 	return NewRequest(&RESTClient{
// 		base:             base,
// 		versionedAPIPath: versionedAPIPath,
// 		content:          content,
// 		Client:           client,
// 	})
// }

// http method
func (r *Request) Verb(verb string) *Request {
	r.verb = verb
	return r
}

// 追加 path 前缀
func (r *Request) Prefix(segments ...string) *Request {
	if r.err != nil {
		return r
	}

	r.pathPrefix = path.Join(r.pathPrefix, path.Join(segments...))

	return r
}

func (r *Request) Suffix(segments ...string) *Request {
	if r.err != nil {
		return r
	}

	r.subpath = path.Join(r.subpath, path.Join(segments...))

	return r
}

func (r *Request) Resource(resource string) *Request {
	if r.err != nil {
		return r
	}

	if len(r.resource) != 0 {
		r.err = fmt.Errorf("resource already set to %q, cannot change to %q", r.resource, resource)
		return r
	}

	if msgs := IsValidPathSegmentName(resource); len(msgs) != 0 {
		r.err = fmt.Errorf("invalid resource %q: %v", resource, msgs)
		return r
	}

	r.resource = resource

	return r
}

func (r *Request) SubResource(subresources ...string) *Request {
	if r.err != nil {
		return r
	}

	if len(r.subresource) != 0 {
		r.err = fmt.Errorf("subresource already set to %q, cannot change to %s", r.resource, subresources)
		return r
	}

	for _, s := range subresources {
		if msgs := IsValidPathSegmentName(s); len(msgs) != 0 {
			r.err = fmt.Errorf("invalid subresource %q: %v", s, msgs)
			return r
		}
	}

	r.subresource = path.Join(subresources...)
	return r
}

func (r *Request) Name(resourceName string) *Request {
	if r.err != nil {
		return r
	}

	if len(resourceName) == 0 {
		r.err = fmt.Errorf("resource name may not be empty")
		return r
	}
	if len(r.resourceName) != 0 {
		r.err = fmt.Errorf("resource name already set to %q, cannot change to %q", r.resourceName, resourceName)
		return r
	}
	if msgs := IsValidPathSegmentName(resourceName); len(msgs) != 0 {
		r.err = fmt.Errorf("invalid resource name %q: %v", resourceName, msgs)
		return r
	}

	r.resourceName = resourceName
	return r
}

// 单独指定 url 中的 path
func (r *Request) AbsPath(segments ...string) *Request {
	if r.err != nil {
		return r
	}

	r.pathPrefix = path.Join(r.c.base.Path, path.Join(segments...))
	return r
}

// 手动指定 url 中的 path 和 query
func (r *Request) RequestURI(uri string) *Request {
	if r.err != nil {
		return r
	}

	locator, err := url.Parse(uri)
	if err != nil {
		r.err = err
		return r
	}

	r.pathPrefix = locator.Path

	if len(locator.Query()) > 0 {
		if r.params == nil {
			r.params = make(url.Values)
		}

		for k, v := range locator.Query() {
			r.params[k] = v
		}
	}

	return r
}

// 添加单个 query 参数
func (r *Request) Param(paramName, s string) *Request {
	if r.err != nil {
		return r
	}

	return r.setParam(paramName, s)
}

func (r *Request) setParam(paramName, value string) *Request {
	if r.params == nil {
		r.params = make(url.Values)
	}

	r.params[paramName] = append(r.params[paramName], value)

	return r
}

// 批量构造 query 参数
func (r *Request) VersionedParams(v interface{}) *Request {
	if r.err != nil {
		return r
	}

	r.c.Client.Query(v)

	return r
}

func (r *Request) Timeout(d time.Duration) *Request {
	if r.err != nil {
		return r
	}

	r.timeout = d

	return r
}

// url 是这几部分构成的：/<pathPrefix>/<resource>/<resourceName>/<subresource>/<subpath>，
// 其中任何一段为空都会被忽略，
// pathPrefix 是在构造方法中设置的，它是配置文件中 apiPath 和 version 的组合，
func (r *Request) URL() *url.URL {
	// /v1
	p := r.pathPrefix
	if len(r.resource) != 0 {
		p = path.Join(p, strings.ToLower(r.resource))
	}
	if len(r.resourceName) != 0 || len(r.subpath) != 0 || len(r.subresource) != 0 {
		p = path.Join(p, r.resourceName, r.subresource, r.subpath)
	}
	finalURL := &url.URL{}
	if r.c.base != nil {
		*finalURL = *r.c.base
	}
	finalURL.Path = p

	// 构造 query 部分
	query := url.Values{}
	for key, values := range r.params {
		for _, value := range values {
			query.Add(key, value)
		}
	}
	// timeout is handled specially here.
	if r.timeout != 0 {
		query.Set("timeout", r.timeout.String())
	}
	finalURL.RawQuery = query.Encode()

	return finalURL
}

func (r *Request) SetHeader(key string, values ...string) *Request {
	if r.headers == nil {
		r.headers = http.Header{}
	}

	r.headers.Del(key)
	for _, value := range values {
		r.headers.Add(key, value)
	}

	return r
}

func (r *Request) Body(obj interface{}) *Request {
	if v := reflect.ValueOf(obj); v.Kind() == reflect.Struct {
		r.SetHeader("Content-Type", r.c.content.ContentType)
	}

	r.body = obj

	return r
}

func (r *Request) Do(ctx context.Context) Result {
	client := r.c.Client
	client.Header = r.headers

	if r.timeout > 0 {
		var cancel context.CancelFunc
		ctx, cancel = context.WithTimeout(ctx, r.timeout)
		defer cancel()
	}
	client.WithContext(ctx)

	zlog.Infow("===", zlog.Any("url", r.URL().String()), zlog.Any("header", client.Header))

	resp, body, errs := client.
		CustomMethod(r.verb, r.URL().String()).
		Send(r.body).
		EndBytes()
	if err := combineErr(resp, body, errs); err != nil {
		return Result{
			response: &resp,
			err:      err,
			body:     body,
		}
	}

	decoder, err := r.c.content.Negotiator.Decoder()
	return Result{
		response: &resp,
		err:      err,
		body:     body,
		decoder:  decoder,
	}
}

type Result struct {
	response *gorequest.Response
	err      error
	body     []byte
	decoder  runtime.Decoder
}

func (r Result) Raw() ([]byte, error) {
	return r.body, r.err
}

func (r Result) Into(v interface{}) error {
	if r.err != nil {
		return r.Error()
	}

	if r.decoder == nil {
		return fmt.Errorf("serializer doesn't exist")
	}

	if err := r.decoder.Decode(r.body, &v); err != nil {
		return err
	}

	return nil
}

func (r Result) Error() error {
	return r.err
}

func combineErr(resp gorequest.Response, body []byte, errs []error) error {
	var e, sep string

	if len(errs) > 0 {
		for _, err := range errs {
			e = sep + err.Error()
			sep = "\n"
		}

		return errors.New(e)
	}

	if resp.StatusCode != http.StatusOK {
		return errors.New(string(body))
	}

	return nil
}

var NameMayNotBe = []string{".", ".."}
var NameMayNotContain = []string{"/", "%"}

func IsValidPathSegmentName(name string) []string {
	for _, illegalName := range NameMayNotBe {
		if name == illegalName {
			return []string{fmt.Sprintf(`may not be '%s'`, illegalName)}
		}
	}

	var errors []string

	for _, illegalContent := range NameMayNotContain {
		if strings.Contains(name, illegalContent) {
			errors = append(errors, fmt.Sprintf(`may not contain '%s'`, illegalContent))
		}
	}

	return errors
}
